////////////////////////////////////////////////////////////////////////////
//
//  Crytek Engine Source File.
//  Copyright (C), Crytek Studios, 2002.
// -------------------------------------------------------------------------
//  File name:   particleeffect.cpp
//  Version:     v1.00
//  Created:     10/7/2003 by Timur.
//  Compilers:   Visual Studio.NET
//  Description: 
// -------------------------------------------------------------------------
//  History:
//
////////////////////////////////////////////////////////////////////////////

#include "StdAfx.h"
#include "ParticleEffect.h"
#include "ParticleEmitter.h"

//////////////////////////////////////////////////////////////////////////
// TypeInfo XML serialisation code

#define TEST_TYPEINFO
#include "ParticleParams_TypeInfo.h"

#ifndef CAFE
DEFINE_INTRUSIVE_LINKED_LIST(CParticleEffect)
#endif

static const int nSERIALIZE_VERSION = 22;
static const int nMIN_SERIALIZE_VERSION = 13;

// SPUs and PPUs are slightly different with floating points
// in the case of the TileInfo, this leads to wrong result with the 1.0f/ x calculation here
// to ensure correct results over ppu and spu, we let the ppu precompute a lookuptable
class CTileLookupTable
{
public:
	CTileLookupTable()
	{
		float div = 1.0f;
		m_LookupTable[0] = 1.0f;
		for( uint32 i = 1 ; i < 256 ; ++i )
		{
			m_LookupTable[i] = 1.0f / div;
			div += 1.0f;
		}
	}

	float Get(uint32 n) { assert(n<256); return m_LookupTable[n]; }

private:
	float m_LookupTable[256];

} _ALIGN(128);

// global object to init the TileLookup Table once
CTileLookupTable gTileLookupTable; 


// Write a struct to an XML node.
template<class T>
void ToXML( IXmlNode& xml, T& val, T& def_val, FToString flags )
{
	CTypeInfo const& info = TypeInfo(&val);
	for AllSubVars( pVar, info )
	{
		if (pVar->GetElemSize() == 0)
			continue;

		if (&pVar->Type == &TypeInfo((void**)0))
			// Never serialize pointers.
			continue;
	
		assert(pVar->GetDim() == 1);

		cstr name = pVar->GetName();

		if (name[0] == '_')
			// Do not serialize private vars.
			continue;

		string str = pVar->ToString(&val, flags, &def_val);
		if (flags._SkipDefault && str.length() == 0)
			continue;

		xml.setAttr(name, str);
	}
}

// Read a struct from an XML node.
template<class T>
void FromXML( IXmlNode& xml, T& val, FFromString flags )
{
	CTypeInfo const& info = TypeInfo(&val);
	int nAttrs = xml.getNumAttributes();
	for (int a = 0; a < nAttrs; a++)
	{
		cstr sKey, sVal;
		xml.getAttributeByIndex(a, &sKey, &sVal);

		CTypeInfo::CVarInfo const* pVar = info.FindSubVar(sKey);
		if (pVar)
		{
			assert(pVar->GetDim() == 1);
			pVar->FromString(&val, sVal, flags);
		}
	}
}

template<class T>
bool GetAttr( IXmlNode const& node, cstr attr, T& val )
{
	cstr sAttr = node.getAttr(attr);
	return *sAttr && TypeInfo(&val).FromString( &val, sAttr, FFromString().SkipEmpty(1) );
}

template<class T>
T GetAttrValue( IXmlNode const& node, cstr attr, T defval )
{
	GetAttr( node, attr, defval );
	return defval;
}

bool IsEnabled( const IXmlNode& node, bool bAll = false, bool bDirectOnly = false )
{
	IXmlNode& nodeParams = *node.findChild("Params");
	if (&nodeParams && GetAttrValue(nodeParams, "Enabled", true))
	{
		// This effect is enabled.
		if (bDirectOnly && GetAttrValue(nodeParams, "SecondGeneration", false))
			return false;
		return true;
	}
	if (bAll)
	{
		// This effect disabled, search for direct children only.
		IXmlNode& nodeChildren = *node.findChild("Childs");
		if (&nodeChildren)
		{
			for (int i = nodeChildren.getChildCount()-1; i >= 0; i--)
			{
				IXmlNode& nodeChild = *nodeChildren.getChild(i);
				if (&nodeChild && IsEnabled(nodeChild, true, true))
					return true;
			}
		}
	}
	return false;
}

template<class T>
void SetParamRange( TVarParam<T>& param, float fValMin, float fValMax )
{
	if (abs(fValMin) > abs(fValMax))
		std::swap(fValMin, fValMax);
	if (fValMax != 0.f)
		param.Set(fValMax, (fValMax - fValMin) / fValMax);
	else
		param.Set(0.f, 0.f);
}

template<class T>
void AdjustParamRange( TVarParam<T>& param, float fAdjust )
{
	float	fValMax = param.GetMaxValue() + fAdjust,
				fValMin = param.GetMinValue() + fAdjust;
	SetParamRange( param, fValMin, fValMax );
}

//////////////////////////////////////////////////////////////////////////
// ResourceParticleParams implementation

static const float fTRAVEL_SAFETY = 0.1f;
static const float fSIZE_SAFETY = 0.1f;

struct SEmitParams
{
	Vec3	vAxis;
	float	fCosMin, fCosMax;
	float fSpeedMin, fSpeedMax;

	bool bOmniDir() const
		{ return fCosMin <= -1.f && fCosMax >= 1.f; }
	bool bSingleDir() const
		{ return fCosMin >= 1.f; }
};

struct SForceParams: SLocalForces
{
	float		fStretch;
};

namespace ParticleEffectUtil
{
	void AddTravelVec( AABB& bb, Vec3 vVel, SForceParams const& force, float fTime )
	{
		Vec3 vTravel(ZERO);
		Travel( vTravel, vVel, fTime, force );
		bb.Add( vTravel );
		if (force.fStretch != 0.f)
		{
			vTravel += vVel * force.fStretch;
			bb.Add( vTravel );
		}
	}

	void AddTravel( AABB& bb, Vec3 const& vVel, SForceParams const& force, float fTime, int nAxes )
	{
		// Add end point.
		AddTravelVec( bb, vVel, force, fTime );

		// Find time of min/max vals of each component, by solving for v[i] = 0
		if (force.fDrag != 0)
		{
			// vt = a/d + w
			// v = (v0-vt) e^(-d t) + vt
			// vs = v + v\t s
			//		= (1 - d s)(v0-vt) e^(-d t) + vt
			//		= 0
			// e^(-d t) = vt / ((1 - d s)(vt-v0))
			// t = -log( vt / ((1 - d s)(vt-v0)) ) / d
			float fInvDrag = 1.f / force.fDrag;
			for (int i = 0; nAxes; i++, nAxes >>= 1)
			{
				if (nAxes & 1)
				{
					float fVT = force.vAccel[i] * fInvDrag + force.vWind[i];
					float d = (fVT - vVel[i]) * (1.f - force.fDrag * force.fStretch);
					if (fVT * d > 0.f)
					{
						float fT = -logf(fVT / d) * fInvDrag;
						if (fT > 0.f && fT < fTime)
							AddTravelVec( bb, vVel, force, fT );
					}
				}
			}
		}
		else
		{
			for (int i = 0; nAxes; i++, nAxes >>= 1)
			{
				if (nAxes & 1)
				{
					if (force.vAccel[i] != 0.f)
					{
						// ps = p + v s
						// vs = p\t + v\t s
						//		= v + a s
						// vs = 0
						//		= v0 + a (t+s)
						// t = -v0/a - s
						float fT = -vVel[i] / force.vAccel[i] - force.fStretch;
						if (fT > 0.f && fT < fTime)
							AddTravelVec( bb, vVel, force, fT );
					}
				}
			}
		}
	}

	Vec3 GetExtremeEmitVec( Vec3 const& vRefDir, SEmitParams const& emit )
	{
		float fEmitCos = vRefDir * emit.vAxis;
		if (fEmitCos >= emit.fCosMin && fEmitCos <= emit.fCosMax)
		{
			// Emit dir is in the cone.
			return vRefDir * emit.fSpeedMax;
		}
		else
		{
			// Find dir in emission cone closest to ref dir.
			Vec3 vEmitPerpX = vRefDir - emit.vAxis*fEmitCos;
			float fPerpLenSq = vEmitPerpX.GetLengthSquared();

			float fCos = clamp_tpl(fEmitCos, emit.fCosMin, emit.fCosMax);
			Vec3 vEmitMax = emit.vAxis*fCos + vEmitPerpX*sqrt_fast_tpl((1.f - fCos*fCos) / (fPerpLenSq+FLT_MIN));
			vEmitMax *= if_pos_else(vEmitMax*vRefDir, emit.fSpeedMin, emit.fSpeedMax);
			return vEmitMax;
		}
	}

	void AddEmitDirs( AABB& bb, Vec3 const& vRefDir, SEmitParams const& emit, SForceParams const& force, float fTime, int nAxes )
	{
		Vec3 vEmit = GetExtremeEmitVec( vRefDir, emit );
		AddTravel( bb, vEmit, force, fTime, nAxes );
		vEmit = GetExtremeEmitVec( -vRefDir, emit );
		AddTravel( bb, vEmit, force, fTime, nAxes );
	}

	inline float GetSinMax(float fCosMin, float fCosMax)
	{
		return fCosMin*fCosMax < 0.f ? 1.f : sqrtf(1.f - min(fCosMin*fCosMin, fCosMax*fCosMax));
	}

	inline float MaxComponent(Vec3 const& v)
	{
		return max(max(abs(v.x), abs(v.y)), abs(v.z));
	}

	// Compute bounds of a cone of emission, with applied gravity.
	void TravelBB( AABB& bb, SEmitParams const& emit, SForceParams const& force, float fTime )
	{
		if (emit.fSpeedMax == 0.f)
		{
			AddTravel( bb, Vec3(0), force, fTime, 7 );
			return;
		}
		else if (emit.bSingleDir())
		{
			AddTravel( bb, emit.vAxis * emit.fSpeedMax, force, fTime, 7 );
			if (emit.fSpeedMin != emit.fSpeedMax)
				AddTravel( bb, emit.vAxis * emit.fSpeedMin, force, fTime, 7 );
			return;
		}

		// First expand box from emission in cardinal directions.
		AddEmitDirs( bb, Vec3(1,0,0), emit, force, fTime, 1 );
		AddEmitDirs( bb, Vec3(0,1,0), emit, force, fTime, 2 );
		AddEmitDirs( bb, Vec3(0,0,1), emit, force, fTime, 4 );

		// Add extreme dirs along gravity.
		if (!force.vAccel.IsZero())
		{
			Vec3 vDir = force.vAccel.GetNormalized();
			if (MaxComponent(vDir) < 0.999f)
				AddEmitDirs( bb, vDir, emit, force, fTime, 0 );
		}

		// And wind.
		if (force.fDrag > 0.f && !force.vWind.IsZero())
		{
			Vec3 vDir = force.vWind.GetNormalized();
			if (MaxComponent(vDir) < 0.999f)
				AddEmitDirs( bb, vDir, emit, force, fTime, 0 );
		}
	}
};

ResourceParticleParams gEmptyParams;

const ResourceParticleParams& CParticleEffect::GetParams() const
{ 
	ResourceParticleParams *ret = m_pParticleParams;

	if (m_pParticleParams == NULL)
	{
		gEmptyParams.bEnabled = false;
		ret = &gEmptyParams;
	}

	return *ret;
}

void ResourceParticleParams::GetStaticBounds( AABB& bb, const QuatTS& loc, float fSpeedScale, const Vec3& vSpawnSize, float fAngMin, float fAngMax, bool bWithSize, float fMaxLife, const SExternForces& forces ) const
{
	fMaxLife = min(fMaxLife, GetMaxParticleLife());

	// Compute spawn source box.
	bb = GetEmitOffsetBounds();

	bb.SetTransformedAABB( Matrix34(loc), bb );
	bb.Expand(vSpawnSize);

	if (eFacing == ParticleFacing_Water)
		// Add slack for positioning emitter above water.
		bb.min.z -= 5.f;

	if (!bSpaceLoop)
	{
		AABB bbTrav;
		GetTravelBounds( bbTrav, fMaxLife, loc, fSpeedScale, fAngMin, fAngMax, forces );
		bb.Augment(bbTrav);
	}

	// Particle size.
	if (bWithSize)
	{
		float fMaxSize = GetMaxVisibleSize(forces);
		fMaxSize *= loc.s * (1.f + fSIZE_SAFETY);
		bb.Expand(Vec3(fMaxSize));
	}
	if (eMoveRelEmitter)
		// Expand a bit for constant movement inaccuracy.
		bb.Expand(Vec3(0.01f));
}

float ResourceParticleParams::GetMaxVisibleSize( SExternForces const& forces ) const
{
	float fMaxSize = fSize.GetMaxValue() * GetMaxObjectSize(pStatObj);
	if (LightSource.fIntensity)
		fMaxSize = max(fMaxSize, LightSource.fRadius.GetMaxValue());
	return fMaxSize;
}

void ResourceParticleParams::GetTravelBounds( AABB& bbResult, float fTime, const QuatTS& loc, float fSpeedScale, float fAngMin, float fAngMax, const SExternForces& forces ) const
{
	bbResult = AABB(0.f);

	if (fTime <= 0.f)
		return;

	// Emission direction.
	SEmitParams emit;
	emit.vAxis = bFocusGravityDir ? -forces.vAccel.GetNormalizedSafe(Vec3(0,0,-1)) : loc.q.GetColumn1();
	float fAngleMax = DEG2RAD(fFocusAngle.GetMaxValue()), fAngleMin = DEG2RAD(fFocusAngle.GetMinValue());
	float fAzimuthMax = DEG2RAD(fFocusAzimuth.GetMaxValue()), fAzimuthMin = DEG2RAD(fFocusAzimuth.GetMinValue());

	if (ePhysicsType > ParticlePhysics_None)
	{
		emit.fCosMax = 1.f;
		emit.fCosMin = -1.f;
	}
	else
	{
		// Max sin and cos of emission relative to emit dir.
		emit.fCosMax = cosf( fAngMin + DEG2RAD(fEmitAngle.GetMinValue()) );
		emit.fCosMin = cosf( min( fAngMax + DEG2RAD(fEmitAngle.GetMaxValue()) + (fAngleMax-fAngleMin + fAzimuthMax-fAzimuthMin)*0.5f, gf_PI ) );
	}

	if (fFocusAngle)
	{
		// Rotate focus about axis.
		emit.vAxis = Quat::CreateRotationAA( (fAzimuthMin+fAzimuthMax)*0.5f, emit.vAxis ) 
		           * ( Quat::CreateRotationAA( (fAngleMin+fAngleMax)*0.5f, loc.q.GetColumn0()) * emit.vAxis );
	}

	emit.fSpeedMin = fSpeed.GetMinValue() * loc.s * fSpeedScale;
	emit.fSpeedMax = fSpeed.GetMaxValue() * loc.s * fSpeedScale;

	SForceParams force;
	force.vWind = forces.vWind;
	force.fStretch = fStretch.GetMaxValue() * max(fStretch.fOffsetRatio+1.f, 0.f);

	// Perform separate checks for variations in drag and gravity.
	float afDrag[2] = { fAirResistance.GetMaxValue(), fAirResistance.GetMinValue() };
	float afGrav[2] = { fGravityScale.GetMaxValue(), fGravityScale.GetMinValue() };
	for (int iDragIndex = (afDrag[1] != afDrag[0] ? 1 : 0); iDragIndex >= 0; iDragIndex--)
	{
		force.fDrag = afDrag[iDragIndex];
		for (int iGravIndex = (afGrav[1] != afGrav[0] ? 1 : 0); iGravIndex >= 0; iGravIndex--)
		{
			force.vAccel = forces.vAccel * afGrav[iGravIndex] + vAcceleration * loc.s;
			ParticleEffectUtil::TravelBB( bbResult, emit, force, fTime );
		}
	}

	if (fTurbulence3DSpeed)
	{
		// Expansion from 3D turbulence.
		float fAccel = fTurbulence3DSpeed.GetMaxValue() * isqrt_tpl(fTime) * (1.f+fTRAVEL_SAFETY);
		SLocalForces forcesTurb;
		forcesTurb.vAccel = Vec3(fAccel);
		forcesTurb.vWind.zero();
		forcesTurb.fDrag = fAirResistance.GetMinValue();
		Vec3 vTrav(ZERO), vVel(ZERO);
		Travel( vTrav, vVel, fTime, forcesTurb );
		bbResult.Expand( vTrav );
	}

	// Expansion from spiral turbulence.
	if (fTurbulenceSpeed)
	{
		float fVortex = fTurbulenceSize.GetMaxValue() * loc.s;
		bbResult.Expand(Vec3(fVortex));
	}

	// Expand by a safety factor.
	bbResult.min *= (1.f+fTRAVEL_SAFETY);
	bbResult.max *= (1.f+fTRAVEL_SAFETY);
}

void ResourceParticleParams::ComputeEnvironmentFlags()
{
	// Needs updated environ if particles interact with gravity or air.
	nEnvFlags = 0;

	if (!bEnabled)
		return;

	if (tVisibleUnderwater != Trinary_Both || eFacing == ParticleFacing_Water)
		nEnvFlags |= ENV_WATER;
	if (fAirResistance)
		nEnvFlags |= ENV_WIND;
	if (fGravityScale || bFocusGravityDir)
		nEnvFlags |= ENV_GRAVITY;
	if (ePhysicsType == ParticlePhysics_SimpleCollision)
	{
		if (bCollideTerrain)
			nEnvFlags |= ENV_TERRAIN;
		if (bCollideStaticObjects)
			nEnvFlags |= ENV_STATIC_ENT;
		if (bCollideDynamicObjects)
			nEnvFlags |= ENV_DYNAMIC_ENT;
	}

	// Rendering params.
	if (fSize && fAlpha)
	{
		if (pStatObj != 0)
			nEnvFlags |= REN_GEOMETRY;
		else if (eFacing == ParticleFacing_Decal)
		{
			if (pMaterial != 0)
				nEnvFlags |= REN_DECAL;
		}
		else
		{
			if (nTexId > 0 || pMaterial != 0)
			{
				nEnvFlags |= REN_SPRITE;
				if (pMaterial)
				{
					SShaderItem const& shader = pMaterial->GetShaderItem();
					if (shader.m_pShader && (shader.m_pShader ->GetFlags() & EF_REFRACTIVE))
						nEnvFlags |= REN_REFRACTION;
				}
			}
		}
		if (bCastShadows)
			nEnvFlags |= REN_CAST_SHADOWS;
	}
	if (LightSource.fIntensity && LightSource.fRadius)
		nEnvFlags |= REN_LIGHTS;
	if (!sSound.empty())
		nEnvFlags |= EFF_SOUND;
	if (eForceGeneration != ParticleForce_None && eForceGeneration != ParticleForce_Target)
		nEnvFlags |= EFF_FORCE;

	//
	// Check whether effect is threadable.
	//
	nEnvFlags |= REN_THREAD;

	// Disable threaded update for any physics interactions.
	if (ePhysicsType >= ParticlePhysics_SimpleCollision)
		// Physicalized particles.
		nEnvFlags &= ~REN_THREAD;
	else if (nEnvFlags & ENV_COLLIDE_PHYSICS)
		// Physics queries for collision.
		nEnvFlags &= ~REN_THREAD;
	
	if (ePhysicsType >= ParticlePhysics_SimplePhysics)			// Physicalized particles
		nEnvFlags |= EFF_DYNAMIC_BOUNDS;

	if (bBindEmitterToCamera)
		nEnvFlags |= REN_BIND_CAMERA;

	// Set flags for RenderElems
	nRenderElemFlags = 0;

	if (!HasVariableVertexCount())
	{
		// Geometry shader can be used for simple sprites.
		// BUG: Work around current rendering errors with materials and texture tiling (2009-09-29).
		if (!pMaterial || TextureTiling.nTilesX*TextureTiling.nTilesY == 0)
			nRenderElemFlags |= FCEF_GEOM_SHADER;
	}
	if (bAllowMerging)
		nRenderElemFlags |= FCEF_MERGABLE;

	//
	// Compute desired and allowed renderer flags.
	//

	nRenObjFlags.Clear();
	nRenObjFlags.SetState(1,
		eBlendType
		+ TextureTiling.bAnimBlend * OS_ANIM_BLEND
		+ bDiffuseCubemap * OS_DIFFUSE_CUBEMAP
		+ bGlobalIllumination * FOB_GLOBAL_ILLUMINATION
		+ bReceiveShadows * FOB_INSHADOW
		+ bNotAffectedByFog * FOB_NO_FOG
		+ bSoftParticle * FOB_SOFT_PARTICLE
		+ bDrawOnTop * OS_NODEPTH_TEST
		+ bDrawNear * FOB_NEAREST
#if PARTICLE_MOTION_BLUR
		+ !!fMotionBlurScale * FOB_MOTION_BLUR_PARTICLE
#endif
		);

	nRenObjFlags.SetState(-1,
		bDrawOnTop * FOB_NEAREST
		+ (TextureTiling.nAnimFramesCount <= 1) * OS_ANIM_BLEND
		+ (bDrawNear || bDrawOnTop || eFacing != ParticleFacing_Camera) * FOB_SOFT_PARTICLE
		+ !fDiffuseLighting * (OS_DIFFUSE_CUBEMAP | FOB_GLOBAL_ILLUMINATION | FOB_INSHADOW)
#if PARTICLE_MOTION_BLUR
		+ (HasVariableVertexCount()) * FOB_MOTION_BLUR_PARTICLE
#endif
		);
}

bool ResourceParticleParams::IsActive() const
{
	if (!bEnabled)
		return false;

	int quality = GetCVars()->e_ParticlesQuality;
	if (quality == 0)
		quality = CONFIG_VERYHIGH_SPEC;
	EConfigSpecBrief eSysConfig = EConfigSpecBrief(quality - CONFIG_LOW_SPEC + ConfigSpec_Low);
	if (eSysConfig < eConfigMin || eSysConfig > eConfigMax)
		return false;
	if (!TrinaryMatch(tDX11, GetRenderer()->GetRenderType() == eRT_DX11))
		return false;
	return true;
}

//////////////////////////////////////////////////////////////////////////
int ResourceParticleParams::LoadResources()
{
	// Load only what is not yet loaded. Check everything, but caller may check params.bResourcesLoaded first.
	// Call UnloadResources to force unload/reload.
	LOADING_TIME_PROFILE_SECTION(gEnv->pSystem);

	if (!bEnabled)
	{
		nEnvFlags = 0;
		return 0;
	}

	if (ResourcesLoaded()|| gEnv->IsDedicated())
	{
		ComputeEnvironmentFlags();
		return 0;
	}

	int nLoaded = 0;

	// Load material.
	if (!pMaterial && nTexId <= 0 && !sMaterial.empty())
	{
		pMaterial = Get3DEngine()->GetMaterialManager()->LoadMaterial( sMaterial.c_str() );
		if (!pMaterial || pMaterial == Get3DEngine()->GetMaterialManager()->GetDefaultMaterial())
		{
			CryWarning( VALIDATOR_MODULE_3DENGINE, VALIDATOR_WARNING, "Particle effect material %s not found", 
				sMaterial.c_str() );

			// Load error texture for artist debugging.
			pMaterial = 0;
			nTexId = GetRenderer()->EF_LoadTexture("!Error", FT_DONT_STREAM)->GetTextureID();
		}
		else
			nLoaded++;
	}
	if (eFacing == ParticleFacing_Decal && sMaterial.empty())
	{
		CryWarning( VALIDATOR_MODULE_3DENGINE, VALIDATOR_WARNING, "Particle effect has no material for decal, texture = %s", 
			sTexture.c_str() );
	}

	// Load texture.
	if (nTexId <= 0 && !sTexture.empty() && !pMaterial)
	{
#if !defined(NULL_RENDERER)
		const uint32 textureLoadFlags = bStreamable ? 0 : FT_DONT_STREAM;
		nTexId = GetRenderer()->EF_LoadTexture(sTexture.c_str(), textureLoadFlags)->GetTextureID();
		if (nTexId <= 0)
			CryWarning( VALIDATOR_MODULE_3DENGINE, VALIDATOR_WARNING, "Particle effect texture %s not found", sTexture.c_str() );
		else
			nLoaded++;
#endif
	}

	// Set aspect ratio.
	if (fTexAspect == 0.f)
	{
		UpdateTextureAspect();
	}

	// Load geometry.
	if (!pStatObj && !sGeometry.empty())
	{
		pStatObj = Get3DEngine()->LoadStatObj(sGeometry.c_str(), NULL, NULL, bStreamable);
		if (!pStatObj)
			CryWarning( VALIDATOR_MODULE_3DENGINE, VALIDATOR_WARNING, "Particle effect geometry not found: %s", sGeometry.c_str() );
		else
			nLoaded++;
	}

	// Process sound
	if (!sSound.empty())
	{
		// TODO needs to be properly replaced by the dependency tracker
		// Only always pre-cache if we're running the editor
		if (gEnv->IsEditor())
			gEnv->pSoundSystem->Precache(sSound.c_str(), 0, FLAG_SOUND_PRECACHE_EVENT_DEFAULT);
	}

	// Precalculate the sound CRC
	crcSoundName = sSound.c_str();

	ComputeEnvironmentFlags();

	nEnvFlags |= EFF_LOADED;
	return nLoaded;
}

//////////////////////////////////////////////////////////////////////////
void ResourceParticleParams::SetTileInfo( float afInfo[4], uint32 nActiveObjFlags ) const
{
	// Tile info: SizeX, SizeY, Count, First
	afInfo[0] = gTileLookupTable.Get(TextureTiling.nTilesX); 
	afInfo[1] = gTileLookupTable.Get(TextureTiling.nTilesY);
	int nFrameScale = TextureTiling.GetFrameCount();
	if (TextureTiling.eAnimCycle != AnimationCycle_Loop && (nActiveObjFlags & OS_ANIM_BLEND))
		nFrameScale--;
	afInfo[2] = (float)(nFrameScale);
	afInfo[3] = (float)TextureTiling.nFirstTile;
}

void ResourceParticleParams::SetMotionBlurInfo( float afInfo[3] ) const
{
#if PARTICLE_MOTION_BLUR
	if (eFacing == ParticleFacing_Camera)
	{
		afInfo[0] = fMotionBlurCamStretchScale;
		afInfo[1] = fMotionBlurStretchScale;
	}
	else
	{
		afInfo[0] = afInfo[1] = 0;
	}
	afInfo[2] = fMotionBlurScale * 0.2f;
#endif
}

void ResourceParticleParams::UpdateTextureAspect()
{
	TextureTiling.Correct();

	fTexAspect = 1.f;
	ITexture* pTexture = GetRenderer()->EF_GetTextureByID(nTexId);
	if (pTexture)
	{
		float fWidth = pTexture->GetWidth() / (float)TextureTiling.nTilesX;
		float fHeight = pTexture->GetHeight() / (float)TextureTiling.nTilesY;
		if (fHeight == 0.f)
			fTexAspect = 0.f;
		else
			fTexAspect = fWidth / fHeight;
	}
}

//////////////////////////////////////////////////////////////////////////
void ResourceParticleParams::UnloadResources()
{
	// To do: Handle materials
	if (nTexId > 0)
	{
		GetRenderer()->RemoveTexture( (uint)nTexId );
		nTexId = -1;
	}
	pStatObj = 0;
	pMaterial = 0;
	fTexAspect = 0.f;
	nEnvFlags &= ~EFF_LOADED;
}

void ResourceParticleParams::Correct( int nVersion, XmlNodeRef paramsNode )
{
	CTypeInfo const& info = ::TypeInfo(this);

	switch (nVersion)
	{
		case 13:
		{
			// Texture tile conversion.
			cstr sRect = paramsNode->getAttr("TextureUVRect");
			if (*sRect)
			{
				RectF rect;
				::TypeInfo(&rect).FromString(&rect, sRect, FFromString().SkipEmpty(1));
				TextureTiling.nTilesX = rect.w > 0.f ? int_round(1.f / rect.w) : 1;
				TextureTiling.nTilesY = rect.h > 0.f ? int_round(1.f / rect.h) : 1;
				TextureTiling.nFirstTile = rect.w*rect.h > 0.f ? int_round((rect.x + rect.y / rect.h) / rect.w) : 0;
			}

			GetAttr(*paramsNode, "TextureVariantCount", TextureTiling.nVariantCount);				
			GetAttr(*paramsNode, "TexAnimFramesCount", TextureTiling.nAnimFramesCount);				
			GetAttr(*paramsNode, "AnimFramerate", TextureTiling.fAnimFramerate);				
			GetAttr(*paramsNode, "AnimCycle", TextureTiling.eAnimCycle);				
		}

		case 14:
			if (string(paramsNode->getAttr("PhysicsType")) == "FullCollision")
				ePhysicsType = ParticlePhysics_SimpleCollision;
			else if (ePhysicsType == ParticlePhysics_SimpleCollision)
				bCollideTerrain = true;

		case 15:
			if (GetAttrValue(*paramsNode, "OnlyUnderWater", false))
				tVisibleUnderwater = Trinary_If_True;
			else if (GetAttrValue(*paramsNode, "OnlyAboveWater", false))
				tVisibleUnderwater = Trinary_If_False;
			if (GetAttrValue(*paramsNode, "OnlyOutdoors", false))
				tVisibleIndoors = Trinary_If_False;

		case 16:
			if (ePhysicsType >= ParticlePhysics_SimpleCollision)
			{
				// Apply previous version's defaults.
				bCollideTerrain = GetAttrValue(*paramsNode, "CollideTerrain", true);
				bCollideStaticObjects = bCollideDynamicObjects = GetAttrValue(*paramsNode, "CollideObjects", true);
			}

		case 17:
		case 18:
			// Rotation fixes.
			// Set 3D particles to Free rotation, matching old behavior. Can now be changed.
			if (!sGeometry.empty() || (sTexture.empty() && sMaterial.empty()))
				eFacing = ParticleFacing_Free;
			if (eFacing == ParticleFacing_Camera)
			{
				// Swap y and z angle vals.
				std::swap(vInitAngles.y, vInitAngles.z);
				std::swap(vRandomAngles.y, vRandomAngles.z);
				std::swap(vRotationRate.y, vRotationRate.z);
				std::swap(vRandomRotationRate.y, vRandomRotationRate.z);
			}

		case 19:
			// Some params combined into structs.
			GetAttr(*paramsNode, "LightSourceRadius", LightSource.fRadius);				
			GetAttr(*paramsNode, "LightSourceIntensity", LightSource.fIntensity);				
			GetAttr(*paramsNode, "LightHDRDynamic", LightSource.fHDRDynamic);				
			GetAttr(*paramsNode, "StretchOffsetRatio", fStretch.fOffsetRatio);				
			GetAttr(*paramsNode, "IgnoreAttractor", TargetAttraction.bIgnore);				
			GetAttr(*paramsNode, "TailSteps", fTailLength.nTailSteps );				

		case 20:
			if (!ePhysicsType && GetAttrValue(*paramsNode, "BindToEmitter", false))
			{
				// Obsolete parameter, set equivalent params.
				eMoveRelEmitter = MoveRelative_Yes;
				vPositionOffset.zero();
				vRandomOffset.zero();
				fSpeed = 0.f;
				fInheritVelocity = 0.f;
				fGravityScale = 0.f;
				fAirResistance = 0.f;
				vAcceleration.zero();
				fTurbulence3DSpeed = 0.f;
				fTurbulenceSize = 0.f;
				fTurbulenceSpeed = 0.f;
				bSpaceLoop = false;
			}

		case 21:
			if (fFocusAzimuth.GetRandomRange() > 0.f)
			{
				// Convert confusing semantics of random variation in previous version.
				if (fFocusAzimuth.GetMaxValue() > 0.f && fFocusAzimuth.GetRandomRange() == 1.f)
				{
					// If a base value was set, assume intention was to vary between 0 and max value, as with most params.
				}
				else
				{
					// Convert previous absolute-360-based random range to standard relative.
					float fValMin = fFocusAzimuth.GetMaxValue();
					float fValMax = fValMin + fFocusAzimuth.GetRandomRange() * 360.f;
					SetParamRange( fFocusAzimuth, fValMin, fValMax );
				}
			}

		case 22:
			// Convert obsolete parameters.
			if (stricmp(paramsNode->getAttr("BlendType"), "ColorBased") == 0)
				eBlendType = ParticleBlendType_Additive;
			else if (stricmp(paramsNode->getAttr("BlendType"), "None") == 0)
				eBlendType = ParticleBlendType_Opaque;

			if (stricmp(paramsNode->getAttr("Facing"), "Horizontal") == 0)
			{
				eFacing = ParticleFacing_Free;
				fEmitAngle.Set(90.f, 0.f);
				vInitAngles.x = vInitAngles.z = 0.f;
				vRotationRate.x = vRotationRate.z = 0.f;
			}

			float fSphereRadius = GetAttrValue(*paramsNode, "PosRandomOffset", 0.f);
			if (fSphereRadius > 0.f)
			{
				// Convert to new roundness fraction.
				vRandomOffset = Vec3(vRandomOffset) + Vec3(fSphereRadius);
				fOffsetRoundness = fSphereRadius / max(max(vRandomOffset.x, vRandomOffset.y), vRandomOffset.z);
			}

			// Replace special-purpose flag with equivalent settings.
			if (GetAttrValue(*paramsNode, "SimpleParticle", false))
			{
				sMaterial = "";
				fEmissiveLighting = 1;
				fEmissiveHDRDynamic = 0;
				fDiffuseLighting = 0;
				fDiffuseBacklighting = 0;
				#if PARTICLE_MOTION_BLUR
					fMotionBlurScale = 0;
				#endif
				bSoftParticle = bReceiveShadows = bCastShadows = bGlobalIllumination = bDiffuseCubemap = false;
				bNotAffectedByFog = true;
			}

			// Convert old misspelling.
			if (!GetAttrValue(*paramsNode, "OctogonalShape", true))
				bOctagonalShape = false;

			// Convert obsolete bias to proper random range.
			float fGravityBias;
			if (GetAttr(*paramsNode, "GravityScaleBias", fGravityBias))
				AdjustParamRange( fGravityScale, fGravityBias );

			// Convert to current spawn enum.
			if (GetAttrValue(*paramsNode, "SecondGeneration", false))
			{
				if (GetAttrValue(*paramsNode, "SpawnOnParentCollision", false))
					eSpawnIndirection = ParticleSpawn_ParentCollide;
				else if (GetAttrValue(*paramsNode, "SpawnOnParentDeath", false))
					eSpawnIndirection = ParticleSpawn_ParentDeath;
				else
					eSpawnIndirection = ParticleSpawn_ParentStart;
			}
	};

	assert(nVersion >= nMIN_SERIALIZE_VERSION && nVersion <= nSERIALIZE_VERSION);

	// Universal corrections.
	if (!fTailLength)
		fTailLength.nTailSteps = 0;

	TextureTiling.Correct();
}

//////////////////////////////////////////////////////////////////////////
// ParticleEffect implementation

//////////////////////////////////////////////////////////////////////////
CParticleEffect::CParticleEffect()
: m_parent(0)
	, m_pParticleParams(0)
#if ENABLE_PARTICLE_EFFECT_LIFETIME_CACHE
	, m_fChildrenLifetime(-1.0f)
#endif
{
}

CParticleEffect::CParticleEffect( const char* sName )
: m_parent(0)
	, m_pParticleParams(0)
	, m_strName(sName)
#if ENABLE_PARTICLE_EFFECT_LIFETIME_CACHE
	, m_fChildrenLifetime(-1.0f)
#endif
{
}

CParticleEffect::CParticleEffect( const char* sName, const ParticleParams &params )
: m_parent(0)
	, m_strName(sName)
#if ENABLE_PARTICLE_EFFECT_LIFETIME_CACHE
	, m_fChildrenLifetime(-1.0f)
#endif
{
	m_pParticleParams = new ResourceParticleParams(params);
	LoadResources(false);
}

//////////////////////////////////////////////////////////////////////////
CParticleEffect::~CParticleEffect()
{
	UnloadResources();
	delete m_pParticleParams;
}

void CParticleEffect::SetEnabled( bool bEnabled )
{
	if (bEnabled != IsEnabled())
	{
		InstantiateParams();
		m_pParticleParams->bEnabled = bEnabled;
		CParticleManager::Instance()->UpdateEmitters(this);
	}
}

//////////////////////////////////////////////////////////////////////////
void CParticleEffect::SetName( cstr sNewName )
{
	if (!m_parent)
	{
		// Top-level effect. Should be fully qualified with library and group names.
		// Remove and reinsert in set.
		if (m_strName != sNewName)
		{
			CParticleManager::Instance()->RenameEffect( this, sNewName );
			m_strName = sNewName;
		}
	}
	else
	{
		// Child effect. Use only final component, and prefix with parent name.
		cstr sBaseName = strrchr(sNewName, '.');
		sBaseName = sBaseName ? sBaseName+1 : sNewName;

		// Ensure unique name.
		stack_string sNewBase;
		for (int i = m_parent->m_childs.size()-1; i >= 0; i--)
		{
			const CParticleEffect* pSibling = &m_parent->m_childs[i];
			if (pSibling != this && pSibling->m_strName == sBaseName)
			{
				// Extract and replace number.
				cstr p = sBaseName+strlen(sBaseName); 
				while (p > sBaseName && (p[-1] >= '0' && p[-1] <= '9'))
					p--;
				int nIndex = atoi(p);
				sNewBase.assign(sBaseName, p);
				sNewBase.append( stack_string(ToString(nIndex+1)) );
				sBaseName = sNewBase;

				// Reset loop.
				i = m_parent->m_childs.size()-1;
			}
		}

		m_strName = sBaseName;
	}
}

//////////////////////////////////////////////////////////////////////////
const char* CParticleEffect::GetBaseName() const
{
	const char* sName = m_strName.c_str();
	if (m_parent)
	{
		// Should only have base name.
		assert(!strchr(sName, '.'));
	}
	else
	{
		// Return everything after lib name.
		if (const char* sBase = strchr(sName, '.'))
			sName = sBase+1;
	}
	return sName;
}

string CParticleEffect::GetFullName() const
{
	if (!m_parent)
		return m_strName;
	return m_parent->GetFullName() + "." + m_strName;
}

//////////////////////////////////////////////////////////////////////////
void CParticleEffect::SetParent( IParticleEffect *pParent )	
{
	if (pParent == m_parent)
		return;

	_smart_ptr<IParticleEffect> ref_ptr = this;
	if (m_parent)
	{
		stl::find_and_erase(m_parent->m_childs, this);
		CParticleManager::Instance()->UpdateEmitters(m_parent);
	}
	if (pParent)
	{
		if (!m_parent)
			// Was previously top-level effect.
			CParticleManager::Instance()->DeleteEffect(this);
		static_cast<CParticleEffect*>(pParent)->m_childs.push_back(this);
	}
	m_parent = static_cast<CParticleEffect*>(pParent);
	CParticleManager::Instance()->UpdateEmitters(this);
}

//////////////////////////////////////////////////////////////////////////
void CParticleEffect::ClearChilds()
{
	m_childs.clear();
	CParticleManager::Instance()->UpdateEmitters(this);
}

//////////////////////////////////////////////////////////////////////////
void CParticleEffect::InsertChild( int slot, IParticleEffect *pEffect )
{
	if (slot < 0)
		slot = 0;
	if (slot > m_childs.size())
		slot = m_childs.size();

	assert( pEffect );
	CParticleEffect* pEff = (CParticleEffect*) pEffect;
	pEff->m_parent = this;
	m_childs.insert( m_childs.begin() + slot, pEff );
	CParticleManager::Instance()->UpdateEmitters(this);
}

//////////////////////////////////////////////////////////////////////////
int CParticleEffect::FindChild( IParticleEffect *pEffect ) const
{
	for_array (i, m_childs)
	{
		if (&m_childs[i] == pEffect)
			return i;
	}
	return -1;
}

CParticleEffect* CParticleEffect::FindChild( const char* szChildName ) const
{
	for_array (i, m_childs)
	{
		if (m_childs[i].m_strName == szChildName)
			return &m_childs[i];
	}

	return 0;
}

//////////////////////////////////////////////////////////////////////////
bool CParticleEffect::LoadResources( bool bAll ) const
{
	int nLoaded = 0;
	if (IsEnabled() && !m_pParticleParams->ResourcesLoaded())
	{
		CRY_DEFINE_ASSET_SCOPE( "Particle Effect", GetFullName() );
		nLoaded = m_pParticleParams->LoadResources();
	}
	if (bAll)
		for_array (i, m_childs)
			nLoaded += (int)m_childs[i].LoadResources(true);
	return nLoaded > 0;
}

void CParticleEffect::UnloadResources( bool bAll ) const
{
	if (m_pParticleParams)
		m_pParticleParams->UnloadResources();
	if (bAll)
		for_all (m_childs).UnloadResources(true);
}

bool CParticleEffect::ResourcesLoaded( bool bAll ) const
{
	if (m_pParticleParams && m_pParticleParams->ResourcesLoaded())
		return true;
	if (bAll)
		for_array (i, m_childs)
			if (m_childs[i].ResourcesLoaded(true))
				return true;
	return false;
}

//////////////////////////////////////////////////////////////////////////
bool CParticleEffect::IsActive( bool bAll ) const
{
	if (!(m_pParticleParams && m_pParticleParams->IsActive()))
		// Disabled in current render context.
		return false;

	if (m_pParticleParams->nEnvFlags & (REN_ANY | EFF_ANY))
		// Has visible or other effects.
		return true;

	for_array (i, m_childs)
		if (bAll || m_childs[i].GetParams().eSpawnIndirection)
			if (m_childs[i].IsActive(true))
				return true;
	return false;
}

void CParticleEffect::SetParticleParams( const ParticleParams &params )
{
	InstantiateParams();
	if (params.sTexture != m_pParticleParams->sTexture || params.sMaterial != m_pParticleParams->sMaterial)
	{
		if (m_pParticleParams->nTexId > 0)
		{
			GetRenderer()->RemoveTexture( (uint)m_pParticleParams->nTexId );
			m_pParticleParams->nTexId = -1;
			m_pParticleParams->fTexAspect = 0.f;
		}
		m_pParticleParams->pMaterial = 0;
		m_pParticleParams->nEnvFlags &= ~EFF_LOADED;
	}
	if (params.sGeometry != m_pParticleParams->sGeometry)
	{
		m_pParticleParams->pStatObj = 0;
		m_pParticleParams->nEnvFlags &= ~EFF_LOADED;
	}
	if (memcmp(&params.TextureTiling, &m_pParticleParams->TextureTiling, sizeof(params.TextureTiling)) != 0)
	{
		m_pParticleParams->fTexAspect = 0.f;
		m_pParticleParams->nEnvFlags &= ~EFF_LOADED;
	}

	static_cast<ParticleParams&>(*m_pParticleParams) = params;
	m_pParticleParams->LoadResources();
	CParticleManager::Instance()->UpdateEmitters(this);
}

//////////////////////////////////////////////////////////////////////////
IParticleEmitter* CParticleEffect::Spawn( const PivotLocation& loc, uint32 uEmitterFlags, const SpawnParams* pSpawnParams )
{
	return CParticleManager::Instance()->CreateEmitter( loc, this, uEmitterFlags, pSpawnParams );
}

//////////////////////////////////////////////////////////////////////////
void CParticleEffect::Serialize( XmlNodeRef node, bool bLoading, bool bChilds )
{
	XmlNodeRef root = node;
	while (root->getParent())
		root = root->getParent();

	if (bLoading)
	{
		if (m_strName.empty())
		{
			if (m_parent)
				// Set simple name, will be automatically qualified with hierarchy.
				SetName( node->getAttr("Name") );
			else
			{
				// Qualify with library name.
				stack_string sFullName = root->getAttr("Name");
				if (sFullName.empty())
					sFullName = root->getTag();
				if (!sFullName.empty())
					sFullName += ".";
				sFullName += node->getAttr("Name");
				SetName(sFullName.c_str());
			}
		}

		int nVersion = nSERIALIZE_VERSION;
		if (root->getAttr( "ParticleVersion", nVersion ))
		{
			if (nVersion < nMIN_SERIALIZE_VERSION || nVersion > nSERIALIZE_VERSION)
			{
				Warning( "Particle Effect %s not loaded: version (%d) out of supported range (%d-%d)",
					GetName(), nVersion, nMIN_SERIALIZE_VERSION, nSERIALIZE_VERSION );
				delete m_pParticleParams;
				m_pParticleParams = 0;
				return;
			}
		}

		XmlNodeRef paramsNode = node->findChild("Params");
		if (paramsNode && (gEnv->IsEditor() || ::IsEnabled(*node, false)))
		{
			// Init params, then read from XML.
			if (!m_pParticleParams)
				m_pParticleParams = new ResourceParticleParams;
			else
			{
				UnloadResources(false);
				m_pParticleParams->~ResourceParticleParams();
				new(m_pParticleParams) ResourceParticleParams;
			}

			ResourceParticleParams& params = *m_pParticleParams;

			if (nVersion < 21)
			{
				// Use old default values.
				params.fCount = 1.f;
				params.fParticleLifeTime = 1.f;
				params.fSpeed = 1.f;
			}

			FromXML( *paramsNode, params, FFromString().SkipEmpty(1).Finalize(!gEnv->IsEditor()) );

			params.Correct( nVersion, paramsNode );

			stack_string soundName(params.sSound.c_str());
			if (params.bEnabled && soundName.Right(4) == ".wav")
			{
				CryLog("Particle Effect %s uses legacy wav sound %s, legacy params ignored", GetName(), params.sSound.c_str());
			}

			if (params.bSpaceLoop && params.fCameraMaxDistance == 0.f && params.GetEmitOffsetBounds().GetVolume() == 0.f)
			{
				Warning( "Particle Effect %s has zero space loop volume: disabled", GetName() );
				params.bEnabled = false;
			}

			if (!params.sSound.empty())
			{
				// Only always pre-cache if we're running the editor
				if (gEnv->IsEditor())
					gEnv->pSoundSystem->Precache(params.sSound.c_str(), 0, FLAG_SOUND_PRECACHE_EVENT_DEFAULT);
			}
		}

		if (bChilds)
		{
			// Serialize childs.
			XmlNodeRef childsNode = node->findChild( "Childs" );
			if (childsNode)
			{
				for (int i = 0; i < childsNode->getChildCount(); i++)
				{
					XmlNodeRef xchild = childsNode->getChild(i);
					if (!gEnv->IsEditor() && !::IsEnabled(*xchild, true))
						// Skip disabled effect branches.
						continue;

					IParticleEffect*	pChildEffect = 0;

					if (cstr sChildName = xchild->getAttr("Name"))
						pChildEffect = FindChild(sChildName);

					if (!pChildEffect)
					{
						pChildEffect = new CParticleEffect();
						pChildEffect->SetParent(this);
					}

					pChildEffect->Serialize( xchild,bLoading,bChilds );
				}
				// m_childs.shrink();
			}
		}
	}
	else
	{
		// Saving.
		node->setAttr( "Name", GetBaseName() );
		root->setAttr( "ParticleVersion", nSERIALIZE_VERSION );

		// Save particle params.
		XmlNodeRef paramsNode = node->newChild( "Params" );

		ParticleParams paramsDef;
		ToXML( *paramsNode, non_const(GetParticleParams()), paramsDef, FToString().SkipDefault(1) );

		if (bChilds && !m_childs.empty())
		{
			// Serialize childs.
			XmlNodeRef childsNode = node->newChild( "Childs" );
			for_array (i, m_childs)
			{
				XmlNodeRef xchild = childsNode->newChild( "Particles" );
				m_childs[i].Serialize( xchild,bLoading,bChilds );
			}
		}
	}
}

void CParticleEffect::GetMemoryUsage(ICrySizer* pSizer) const
{	
	if (!pSizer->AddObjectSize(this))
		return;

	pSizer->AddObject(m_strName);
	pSizer->AddObject(m_childs);
	pSizer->AddObject(m_pParticleParams);	
}

void CParticleEffect::GetEffectCounts( SEffectCounts& counts ) const
{
	counts.nLoaded++;
	if (IsEnabled())
		counts.nEnabled++;
	if (GetParams().ResourcesLoaded())
		counts.nUsed++;
	if (IsActive())
		counts.nActive++;

	for_array (i, m_childs)
		m_childs[i].GetEffectCounts(counts);
}

CParticleEffect* CParticleEffect::GetIndirectParent() const
{
	for (CParticleEffect const* pEffect = this; pEffect; pEffect = pEffect->m_parent)
		if (pEffect->GetParams().eSpawnIndirection)
			return pEffect->m_parent;
	return 0;
}

SPU_NO_INLINE float CParticleEffect::GetMaxEffectLife( FEffectLife opts ) const
{
	float fLife = 0.f;
	if (IsActive())
	{
		if (opts.fEmitterMaxLife() > 0.f)
		{
			const ResourceParticleParams& params = GetParams();
			fLife = params.fPulsePeriod ? fHUGE : params.GetMaxEmitterLife();
			if (const CParticleEffect* pParent = GetIndirectParent())
			{
				float fParentLife = pParent->GetParams().GetMaxParticleLife();
				fLife = min(fLife, fParentLife);
			}
			fLife = min(fLife, opts.fEmitterMaxLife());
		}
		if (opts.bParticleLife())
			fLife += GetParams().GetMaxParticleLife();
	}

	if (opts.bAllChildren() || opts.bIndirectChildren())
	{
		for_array (i, m_childs)
		{
			if (m_childs[i].GetParams().eSpawnIndirection)
				fLife = max(fLife, m_childs[i].GetMaxEffectLife(FEffectLife(opts).fEmitterMaxLife(fHUGE).bAllChildren(1)));
			else if (opts.bAllChildren())
				fLife = max(fLife, m_childs[i].GetMaxEffectLife(opts));
		}
	}
	return fLife;
}

float CParticleEffect::GetEquilibriumAge(bool bAll) const
{
	float fEquilibriumAge = 0.f;
	bool bHasEquilibrium = IsActive() && GetParams().HasEquilibrium();
	if (bHasEquilibrium)
	{
		fEquilibriumAge = GetParams().fSpawnDelay.GetMaxValue() + GetMaxParticleFullLife();
		if (const CParticleEffect* pParent = GetIndirectParent())
			fEquilibriumAge += pParent->GetEquilibriumAge(false);
	}

	if (bAll)
	{
		for_array (i, m_childs)
			if (!m_childs[i].GetParams().eSpawnIndirection)
				fEquilibriumAge = max(fEquilibriumAge, m_childs[i].GetEquilibriumAge(true));
	}

	return fEquilibriumAge;
}

#include UNIQUE_VIRTUAL_WRAPPER(IParticleEffect)

